package fi.iki.juri.units;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

/**
 * A RPN calculator.
 * 
 * @author Juri Pakaste <juri@iki.fi>
 */
public class Calculator {
	private final List<Operation> expression;
	private final String originalExpression;

	private Calculator(final String originalExpression, final List<Operation> expression) {
		this.originalExpression = originalExpression;
		this.expression = expression;
	}

	/**
	 * Executes the Calculator on the given value.
	 *  
	 * @param value A double to initialize the stack with
	 * @return The final value of the calculation
	 * @throws ExpressionException If the execution left zero or more than one items on the stack
	 */
	public double calc(final double value) throws ExpressionException {
		final Stack<Number> stack = new Stack<Number>();
		stack.add(new Double(value));
		for (final Operation operation : expression) {
			operation.operate(stack);
		}
		if (stack.size() != 1)
			throw new ExpressionException("Expected only one number on stack after expression evaluation, had " + stack.size());
		return stack.pop().doubleValue();
	}

	public String toString() {
		return "<Calculator: " + originalExpression + ">";
	}
	
	/**
	 * Creates a calculator from the expression.
	 * 
	 * @param expression
	 * @return
	 */
	public static Calculator create(final String expression) {
		return new Calculator(expression, parseExpression(expression));
	}

	/**
	 * Creates a reversed calculator from the expression.
	 * 
	 * This is extremely simplistic and won't handle anything much more complicated than 1000 * -> 1000 /.
	 * 
	 * @param fromExpression
	 * @return
	 */
	public static Calculator createReverse(final String fromExpression) {
		return new Calculator(fromExpression + " (r)", reverseExpression(parseExpression(fromExpression)));
	}

	/**
	 * Reverse expression.
	 * 
	 * This is extremely simplistic and won't handle anything much more complicated than 1000 * -> 1000 /.
	 * @param expression
	 * @return
	 */
	private static List<Operation> reverseExpression(final List<Operation> expression) {
		final List<Operation> reversed = new ArrayList<Operation>(expression.size());
		for (final Operation part : expression) {
			if (part instanceof PlusOperator) {
				reversed.add(OperatorFactory.getMinusOperator());
			} else if (part instanceof MinusOperator) {
				reversed.add(OperatorFactory.getPlusOperator());
			} else if (part instanceof TimesOperator) {
				reversed.add(OperatorFactory.getDivisionOperator());
			} else if (part instanceof DivisionOperator) {
				reversed.add(OperatorFactory.getTimesOperator());
			} else {
				reversed.add(part);
			}
		}
		return reversed;
	}
	
	private static List<Operation> parseExpression(final String expression) throws ExpressionFormatException {
		final String[] parts = expression != null ? expression.split(" ") : new String[] {};
		final List<Operation> parsed = new ArrayList<Operation>(parts.length);
		for (final String part : parts) {
			if ("+".equals(part)) {
				parsed.add(OperatorFactory.getPlusOperator());
				continue;
			} else if ("-".equals(part)) {
				parsed.add(OperatorFactory.getMinusOperator());
				continue;
			} else if ("*".equals(part)) {
				parsed.add(OperatorFactory.getTimesOperator());
				continue;
			} else if ("/".equals(part)) {
				parsed.add(OperatorFactory.getDivisionOperator());
				continue;
			} else {
				try {
					parsed.add(new ValueOperation(Integer.valueOf(part)));
					continue;
				} catch (NumberFormatException ex) {}
				try {
					parsed.add(new ValueOperation(Double.valueOf(part)));
					continue;
				} catch (NumberFormatException ex) {}
			}
			throw new ExpressionFormatException(String.format("Unrecognized part \"%1$s\" in expression \"%2$s\"", part, expression));
		}
		return parsed;
	}

	public static Calculator createEmptyCalculator() {
		return new Calculator("", new ArrayList<Operation>(0));
	}
}

interface Operation {
	void operate(final Stack<Number> stack);
}

class ValueOperation implements Operation {
	private final Number number;

	public ValueOperation(final Number number) {
		this.number = number;
	}
	
	public void operate(Stack<Number> stack) {
		stack.push(number);
	}
}

class OperatorFactory {
	private static PlusOperator plusOperator = null;
	private static MinusOperator minusOperator = null;
	private static TimesOperator timesOperator = null;
	private static DivisionOperator divisionOperator = null;
	
	public static PlusOperator getPlusOperator() {
		if (plusOperator == null)
			plusOperator = new PlusOperator();
		return plusOperator;
	}
	
	public static MinusOperator getMinusOperator() {
		if (minusOperator == null)
			minusOperator = new MinusOperator();
		return minusOperator;
	}
	
	public static TimesOperator getTimesOperator() {
		if (timesOperator == null)
			timesOperator = new TimesOperator();
		return timesOperator;
	}
	
	public static DivisionOperator getDivisionOperator() {
		if (divisionOperator == null)
			divisionOperator = new DivisionOperator();
		return divisionOperator;
	}
}

class PlusOperator implements Operation {
	public void operate(final Stack<Number> stack) {
		stack.push(new Double(stack.pop().doubleValue() + stack.pop().doubleValue()));
	}
}

class MinusOperator implements Operation {
	public void operate(final Stack<Number> stack) {
		final double first = stack.pop().doubleValue();
		final double second = stack.pop().doubleValue();
		stack.push(new Double(second - first));
	}
}

class TimesOperator implements Operation {
	public void operate(final Stack<Number> stack) {
		stack.push(new Double(stack.pop().doubleValue() * stack.pop().doubleValue()));
	}
}

class DivisionOperator implements Operation {
	public void operate(final Stack<Number> stack) {
		final double first = stack.pop().doubleValue();
		final double second = stack.pop().doubleValue();
		stack.push(new Double(second / first));
	}
}
